import numpy as np
import pandas as pd
import jieba
import os
import pickle
import tkinter as tk


# 定义朴素贝叶斯分类器类
class NaiveBayesClassifier:
    def __init__(self):
        self.priors = None
        self.conditionals = None

    # 训练朴素贝叶斯分类器
    def fit(self, X, y):
        num_classes = len(np.unique(y))  # 获取类别数
        num_features = X.shape[1]  # 获取特征数
        self.priors = np.zeros(num_classes)  # 初始化先验概率
        self.conditionals = np.zeros((num_classes, num_features))  # 初始化条件概率
        # 计算先验概率和条件概率
        for i in range(num_classes):
            X_i = X[y == i]
            self.priors[i] = X_i.shape[0] / X.shape[0]
            self.conditionals[i] = (X_i.sum(axis=0) + 1) / (X_i.sum() + num_features)

    # 预测分类结果
    def predict(self, X):
        log_probs = np.log(self.priors) + X @ np.log(self.conditionals.T)
        return np.argmax(log_probs, axis=1)


# 过滤姓名中的非法字符
def filter_name(name):
    name = ''.join(filter(lambda x: x.isalpha() or x.isalnum(), name))  # 只保留字母和数字
    return name


# 读取训练集和测试集数据
train_data = pd.read_csv('NameData.txt', sep='\t', header=None, names=['name', 'gender'],
                         encoding='utf-8')  # 读取训练集数据
test_data = pd.read_csv('NameTestData.txt', sep='\t', header=None, names=['name', 'gender']
                        , encoding='utf-8')  # 读取测试集数据

# 对姓名进行分词和过滤非法字符，并将性别转换为数字标签
train_data['name'] = train_data['name'].apply(lambda x: filter_name(' '.join(jieba.lcut(x))))  # 分词并过滤非法字符
train_data['gender'] = train_data['gender'].apply(lambda x: 0 if x == '女' else 1)  # 将性别转换为数字标签

test_data['name'] = test_data['name'].apply(lambda x: filter_name(' '.join(jieba.lcut(x))))  # 分词并过滤非法字符
test_data['gender'] = test_data['gender'].apply(lambda x: 0 if x == '女' else 1)  # 将性别转换为数字标签


# 将姓名转换为特征向量
def build_word_vector(text, filter_words):
    text = list(text)
    word_vector = {word: 0 for word in filter_words}
    for word in text:
        if word in word_vector:
            word_vector[word] += 1
    return pd.Series(word_vector)


# 如果特征向量和分类器已经存在，则直接读取
if os.path.exists('all_vectors.pkl'):
    with open('filter_words.txt', 'r', encoding='utf-8') as f:
        filter_words = sorted(set([line.strip() for line in f.readlines()]))  # 读取特征词
    with open('all_vectors.pkl', 'rb') as f:
        all_vectors = pickle.load(f)  # 读取特征向量
# 否则，生成特征向量和分类器，并保存到文件中
else:
    # 合并训练集和测试集数据，并统计每个汉字的出现频率
    train_name_list = train_data['name'].tolist()
    test_name_list = test_data['name'].tolist()

    name_list = train_name_list + test_name_list  # 合并训练集和测试集数据
    word_freq_dict = {}
    for name in name_list:
        name = list(name)
        for word in name:
            if word in word_freq_dict:
                word_freq_dict[word] += 1
            else:
                word_freq_dict[word] = 1

    word_freq_df = pd.DataFrame({'word': list(word_freq_dict.keys()), 'freq': list(word_freq_dict.values())})
    word_freq_df.sort_values(by='freq', ascending=False, inplace=True)

    # 选取出现频率大于等于 5 的汉字作为特征词
    filter_words = [' '] + word_freq_df[word_freq_df['freq'] >= 5]['word'].tolist()
    filter_words = sorted(set(filter_words))

    # 将每个姓名转换为特征向量
    all_vectors = pd.concat([train_data['name'], test_data['name']], axis=0)
    all_vectors = all_vectors.apply(lambda x: build_word_vector(x, filter_words)).fillna(0)

    # 将特征词和特征向量保存到文件中
    with open('filter_words.txt', 'w', encoding='utf-8') as f:
        f.write('\n'.join(filter_words))
    with open('all_vectors.pkl', 'wb') as f:
        pickle.dump(all_vectors, f)


# 将训练集和测试集的特征向量分开
train_vectors = all_vectors.iloc[:len(train_data), :]
test_vectors = all_vectors.iloc[len(train_data):, :]

# 如果分类器已经存在，则直接读取
if os.path.exists('gender_classifier.pkl'):
    with open('gender_classifier.pkl', 'rb') as f:
        clf = pickle.load(f)  # 读取分类器
# 否则，训练分类器，并保存到文件中
else:
    clf = NaiveBayesClassifier()
    clf.fit(train_vectors.values, train_data['gender'].values)  # 训练分类器
    with open('gender_classifier.pkl', 'wb') as f:
        pickle.dump(clf, f)

# 在测试集上测试分类器的准确率
y_pred = clf.predict(test_vectors.values)
accuracy = np.mean(y_pred == test_data['gender'].values)


# 创建GUI界面
def predict_gender():
    name = input_name_Text.get(1.0, tk.END).strip()  # 获取输入的姓名
    name = filter_name(' '.join(jieba.lcut(name)))  # 分词并过滤非法字符
    vector = build_word_vector(name, filter_words).values.reshape(1, -1)  # 将姓名转换为特征向量
    gender = '女' if clf.predict(vector)[0] == 0 else '男'  # 预测性别
    sex_Text.delete(1.0, tk.END)  # 清空预测性别显示框
    sex_Text.insert(1.0, gender)  # 将预测性别显示在文本框中


# 实现界面化
root = tk.Tk()  # 创建窗口
root.title('听其名知其性性别预测模型')
root.geometry('500x300+380+100')  # 设置窗口大小和位置

auther_lable = tk.Label(root, text="@Author:", font=('宋体', 13), width=10)  # 显示作者信息
auther_lable.place(x=120, y=10, anchor='nw')
name_lable = tk.Label(root, text="聪明的波波", font=('宋体', 13), width=10)
name_lable.place(x=250, y=10, anchor='nw')

accuracy_lable = tk.Label(root, text="测试准确率:", font=('Arial', 15), width=10)  # 显示测试准确率
accuracy_lable.place(x=75, y=50, anchor='nw')
accuracy_Text = tk.Text(root, width=20, height=1, bg="white", fg="black", font=("宋体", 14), bd='0')  # 测试准确率输出框
accuracy_Text.place(x=190, y=50, anchor='nw')
accuracy_Text.insert(1.0, '{:.2%}'.format(accuracy))  # 输出测试准确率

input_name_lable = tk.Label(root, text="请输入姓名:", font=('Arial', 15), width=10)  # 显示输入框标签
input_name_lable.place(x=75, y=100, anchor='nw')
input_name_Text = tk.Text(root, width=20, height=1, bg="white", fg="black", font=("宋体", 14), bd='0')  # 中文人名录入框
input_name_Text.place(x=190, y=100, anchor='nw')

sex_label = tk.Label(root, text="判定性别为:", font=('Arial', 15), width=10)  # 显示预测性别标签
sex_label.place(x=75, y=150, anchor='nw')
sex_Text = tk.Text(root, width=20, height=1, bg="white", fg="black", font=("宋体", 14), bd='0')  # 预测性别显示框
sex_Text.place(x=190, y=150, anchor='nw')

# 按钮
sex_button = tk.Button(root, text="性别预测", bg="lightblue", width=10, font=('黑体', 12),
                       command=predict_gender)  # 调用内部方法  加()为直接调用
sex_button.place(x=100, y=200, anchor='nw')
quit_button = tk.Button(root, text="退   出", bg="lightblue", width=10, font=('黑体', 12),
                        command=root.quit)  # 调用内部方法加()为直接调用
quit_button.place(x=280, y=200, anchor='nw')


root.mainloop()
